import json
import logging
import os
import socket
import time
from datetime import datetime
from pathlib import Path
from socket import timeout as TimeoutException
from threading import Thread


# def client(host: str, port: int, extra: str = '') -> None:
#     """
#     Initialize 2 threads: 1 main thread for sending $host:$port infinitely, 1 sub thread for receiving and logging.
#     :param host: target host ip, e.g. '192.168.11.19'
#     :param port: target host port, e.g. 3270
#     :param extra: extra message to append
#     :return: None
#     """
#     # Initialize socket
#     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#     sock.settimeout(1)
#
#     # Initialize log dir folder
#     root = Path('.')
#     log_dir = root / 'log'
#     os.makedirs(log_dir, exist_ok=True)
#     print(f'log dir: {log_dir}')
#
#     # Initialize response message
#     message = {'client': 'client_message'} if not extra else {'client': 'client_message', 'extra': f'{extra}'}
#     message = json.dumps(message).encode()
#
#     def listen(skt: socket.socket) -> None:
#         # Initialize logger info
#         logging.basicConfig(filename=str(log_dir / (get_time() + '.txt')), format='%(message)s', level=logging.INFO)
#         logging.info('------ client up ------')
#         logging.info(f'------ remote {host}:{port} ------')
#
#         t0 = time.time()
#         bind_port_count = 0
#         while True:
#             t = get_time()
#             try:
#                 data, _ = skt.recvfrom(1024)
#                 if data:
#                     data = json.loads(data)
#                     t1 = time.time()
#                     print(f'{t} - {data}')
#
#                     # Log for every 3 sec if data is received
#                     if t1 - t0 >= 1:
#                         logging.info(f'{t} - {data}')
#                         t0 = t1
#                 # If nothing received for a while, we bind the port again
#                 else:
#                     if not bind_port_count:
#                         bind_port_count = time.time()
#                     else:
#                         if time.time() - bind_port_count >= 1:
#
#
#             except TimeoutException:
#                 print(f'{t} - RecvTimeout')
#                 t1 = time.time()
#
#                 # Log for every 3 sec if data is not received
#                 if t1 - t0 >= 1:
#                     logging.info(f'{t} - RecvTimeout')
#                     t0 = t1
#                 continue
#
#     # Initialize and start the listening thread
#     th = Thread(target=listen, args=(sock,), daemon=True)
#     th.start()
#
#     # Send the message infinitely
#     while True:
#         sock.sendto(message, (host, port))
#         time.sleep(0.05)

def client(host: str, port: int, extra: str = '') -> None:
    """
    This is a rollback function.
    :param host:
    :param port:
    :param extra:
    :return:
    """
    # Initialize response message
    message = {'client': 'client_message'} if not extra else {'client': 'client_message', 'extra': f'{extra}'}
    message = json.dumps(message).encode()

    # Initialize socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1)

    # Initialize log dir folder
    root = Path('.')
    log_dir = root / 'log'
    os.makedirs(log_dir, exist_ok=True)
    print(f'log dir: {log_dir}')

    # Initialize logger info
    logging.basicConfig(filename=str(log_dir / (get_time() + '.txt')), format='%(message)s', level=logging.INFO)
    logging.info('------ client up ------')
    logging.info(f'------ remote {host}:{port} ------')

    t0 = time.time()
    while True:
        t = get_time()
        sock.sendto(message, (host, port))
        try:
            data, _ = sock.recvfrom(1024)
            data = json.loads(data)
            t1 = time.time()
            print(f'{t} - {data}')
            if t1 - t0 >= 1:
                logging.info(f'{t} - {data}')
                t0 = t1

        except TimeoutException:
            print(f'{t} - RecvTimeout')
            logging.info(f'{t} - RecvTimeout')
            # Rebind port
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.settimeout(1)
            print('New port bound')
            continue


def server(port: int) -> None:
    """
    Listening on $port infinitely. If server receives anything, log it and send a response message.
    :param port: Bound port for localhost
    :return: None
    """

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', port))
    sock.settimeout(1)
    print('UDP server up and listening')

    # Initialize log dir folder
    root = Path('.')
    log_dir = root / 'log'
    os.makedirs(log_dir, exist_ok=True)
    print(f'log dir: {log_dir}')

    # Initialize logger info
    logging.basicConfig(filename=str(log_dir / (get_time() + '.txt')), format='%(message)s', level=logging.INFO)
    logging.info('------ server up ------ ')
    logging.info(f'------ local port: {port} ------')

    response = json.dumps({'server': 'server_message'}).encode()

    t0 = time.time()
    while True:
        t = get_time()
        try:
            data, addr = sock.recvfrom(1024)
            if data:
                data = json.loads(data)
                t1 = time.time()
                print(f'{t} - {addr[0]}:{addr[1]} - {data}')
                if t1 - t0 >= 1:
                    logging.info(f'{t} - {addr[0]}:{addr[1]} - {data}')
                    t0 = t1
                sock.sendto(response, addr)
        except TimeoutException:
            t1 = time.time()
            print(f'{t} - RecvTimeout')
            if t1 - t0 >= 1:
                logging.info(f'{t} - RecvTimeout')
                t0 = t1
            continue


def bytes2string(b: bytes) -> str:
    """
    Deprecated.
    This is a function converts bytes to a string.
    :param b: Bytes, e.g. b'\x17\x18\x19'
    :return: A string, e.g. '23 24 25'
    """
    return ' '.join(map(str, list(b)))


def string2bytes(s: str) -> bytes:
    """
    Deprecated.
    This is a function converts a string to bytes.
    :param s: A string, e.g. '23 24 25'
    :return: Bytes, e.g. b'\x17\x18\x19'
    """
    return bytes(list(map(int, s.split())))


def get_time() -> str:
    """
    This is a function outputs a string of the current time.
    :return: A string of time, e.g. 20210408_173350, standing for 2021-04-08 17:33:50
    """
    return datetime.now().strftime('%Y%m%d_%H%M%S')


def hp_parser(ip: str) -> tuple:
    """
    This is a function for parsing client() parameters.
    :param ip: Regular string of IP with port, e.g. '192.168.0.98:3366'
    :return: A string of host and an integer of port, e.g. '192.168.0.98', 3366
    """
    host, port = ip.split(':')
    return host, int(port)


def sleep(t: int) -> None:
    """
    To test CPE connection, we use a sleep() function to test if NAT is correct after sleeping for a while.
    :param t: time of seconds, e.g. 120, standing for 120 seconds
    :return: None
    """
    for i in range(0, t):
        print(f'Countdown: {t - i}')
        time.sleep(1)
